package org.jetlinks.pro.auth.web;

import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.events.AuthorizationSuccessEvent;
import org.hswebframework.web.authorization.token.UserTokenManager;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.pro.ValueObject;
import org.jetlinks.pro.auth.entity.ThirdPartyUserBindEntity;
import org.jetlinks.pro.auth.sso.SsoProperties;
import org.jetlinks.pro.auth.sso.ThirdPartyProvider;
import org.jetlinks.pro.auth.sso.ThirdPartyProviderSupport;
import org.jetlinks.pro.auth.sso.oauth2.CommonOAuth2SsoProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

@RestController
@RequestMapping("/sso")
@Authorize(ignore = true)
@Tag(name = "单点登录")
public class SsoController {

    private final Map<String, ThirdPartyProvider> providers = new HashMap<>();

    private final SsoProperties properties;

    private final ReactiveRedisOperations<String, String> redis;

    private final ReactiveRepository<ThirdPartyUserBindEntity, String> bindRepository;

    private final UserTokenManager userTokenManager;

    public SsoController(ReactiveRedisConnectionFactory connectionFactory,
                         SsoProperties properties,
                         @SuppressWarnings("all")
                             ReactiveRepository<ThirdPartyUserBindEntity, String> bindRepository,
                         UserTokenManager userTokenManager,
                         @Autowired(required = false) List<ThirdPartyProvider> providers,
                         ObjectProvider<ThirdPartyProviderSupport> supports) {
        this.redis = new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string());
        this.bindRepository = bindRepository;
        this.userTokenManager = userTokenManager;
        this.properties = properties;

        for (ThirdPartyProviderSupport support : supports) {
            for (ThirdPartyProvider provider : support.getProviders()) {
                this.providers.put(provider.getId(), provider);
            }
        }
        if(CollectionUtils.isNotEmpty(providers)) {
            for (ThirdPartyProvider provider : providers) {
                this.providers.put(provider.getId(), provider);
            }
        }
    }

    @Getter
    @Setter
    public static class BindCode {
        private String provider;

        private String providerName;

        private ThirdPartyProvider.NotifyResult result;
    }

    @EventListener
    //处理登录时的bind请求
    public void handleAuthBindEvent(AuthorizationSuccessEvent event) {

        String bindCode = event.getParameter("bindCode").map(String::valueOf).orElse("");
        if (StringUtils.isEmpty(bindCode)) {
            return;
        }
        String redisKey = "sso_bind_code:" + bindCode;
        event.async(
            redis
                .opsForValue()
                .get(redisKey)
                .map(bind -> JSON.parseObject(bind, BindCode.class))
                .flatMap(code -> {
                    //保存绑定关系
                    ThirdPartyUserBindEntity bindEntity = new ThirdPartyUserBindEntity();
                    bindEntity.setBindTime(System.currentTimeMillis());
                    bindEntity.setProvider(code.getProvider());
                    bindEntity.setProviderName(code.getProviderName());
                    bindEntity.setDescription(code.getResult().getDescription());
                    bindEntity.setUserId(event.getAuthentication().getUser().getId());
                    bindEntity.setThirdPartyUserId(code.getResult().getThirdPartyUserId());
                    bindEntity.setOthers(code.getResult().getOthers());
                    bindEntity.generateId();
                    return bindRepository.save(bindEntity);
                })
                .then(redis.delete(redisKey))
        );

    }

    @GetMapping("/providers")
    @Operation(summary = "获取支持的SSO服务商标识")
    public Flux<String> getProviders() {
        return Flux.fromIterable(providers.keySet());
    }

    @GetMapping("/{provider}/login")
    @Operation(summary = "跳转第三方登录")
    public Mono<Void> redirectSsoLogin(@PathVariable
                                       @Parameter(description = "SSO服务商标识") String provider,
                                       ServerWebExchange exchange) {
        ThirdPartyProvider partyProvider = providers.get(provider);

        if (partyProvider == null) {
            throw new UnsupportedOperationException("unsupported:" + provider);
        }
        String notifyUrl = properties.getBaseUrl() + "/sso/" + provider + "/notify";

        URI url = partyProvider.getLoginUrl(notifyUrl);

        return Mono.fromRunnable(() -> {
            //重定向到登录地址
            exchange.getResponse().setStatusCode(HttpStatus.FOUND);
            exchange.getResponse().getHeaders().setLocation(url);
        });
    }

    private Mono<BindResult> doHandleNotify(String provider,
                                            Map<String, ?> notifyParameter) {
        ThirdPartyProvider partyProvider = providers.get(provider);

        if (partyProvider == null) {
            throw new BusinessException("error.unsupported_provider", 500, provider);
        }
        ValueObject parameters = ValueObject.of(notifyParameter);

        return partyProvider
            .handleNotify(parameters)
            .filter(result -> StringUtils.hasText(result.getThirdPartyUserId()))
            .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("error.unsupported_notify")))
            .flatMap(notifyResult -> bindRepository
                .createQuery()
                .where(ThirdPartyUserBindEntity::getProvider, provider)
                .and(ThirdPartyUserBindEntity::getThirdPartyUserId, notifyResult.getThirdPartyUserId())
                .fetchOne()
                .map(bind -> {
                    //已经绑定了用户
                    String token = IDGenerator.MD5.generate();
                    return userTokenManager
                        .signIn(token, "sso-" + provider, bind.getUserId(), notifyResult.getExpiresMillis())
                        .then(Mono.fromSupplier(() -> BindResult.of(true, null, token, bind.getUserId())));
                })
                .defaultIfEmpty(Mono.defer(() -> {
                    //未绑定用户
                    String bindCode = IDGenerator.MD5.generate();
                    BindCode bindCodeResult = new BindCode();
                    bindCodeResult.setProvider(provider);
                    bindCodeResult.setProviderName(partyProvider.getName());
                    bindCodeResult.setResult(notifyResult);
                    return redis
                        .opsForValue()
                        .set("sso_bind_code:" + bindCode, JSON.toJSONString(bindCodeResult), Duration.ofMinutes(5))
                        .then(Mono.fromSupplier(() -> BindResult.of(false, bindCode, null, null)));
                }))
                .flatMap(Function.identity()));
    }

    private Mono<Void> doHandleNotify(String provider,
                                      ServerWebExchange exchange,
                                      Map<String, String> notifyParameter) {
        ThirdPartyProvider partyProvider = providers.get(provider);

        if (partyProvider == null) {
            throw new BusinessException("error.unsupported_provider", 500, provider);
        }
        ValueObject parameters = ValueObject.of(notifyParameter);
        String sourceRedirect = parameters.getString("redirect", properties.getBaseUrl());

        return this
            .doHandleNotify(provider, notifyParameter)
            .doOnNext(result -> {
                if (result.isBound()) {
                    String redirect = properties.getTokenSetPageUrl();
                    //重定向到设置token的地址
                    URI uri = URI.create(redirect + "?token=" + result.getToken() + "&redirect=" + sourceRedirect);
                    exchange.getResponse().setStatusCode(HttpStatus.FOUND);
                    exchange.getResponse().getHeaders().setLocation(uri);
                } else {
                    String redirect = properties.getBindPageUrl();
                    //重定向到绑定地址
                    URI uri = URI.create(redirect + "?code=" + result.getBindCode() + "&redirect=" + sourceRedirect);
                    exchange.getResponse().setStatusCode(HttpStatus.FOUND);
                    exchange.getResponse().getHeaders().setLocation(uri);
                }
            })
            .then();

    }

    @Getter
    @Setter
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class BindResult {
        @Schema(description = "是否已绑定用户")
        private boolean bound;
        @Schema(description = "未绑定用户时的绑定码")
        private String bindCode;
        @Schema(description = "已绑定用户的token")
        private String token;
        @Schema(description = "已绑定用户ID")
        private String userId;

    }

    @GetMapping("/{provider}/notify")
    @Operation(summary = "(GET)登录结果通知并跳转页面")
    public Mono<Void> handleGetNotify(@PathVariable
                                      @Parameter(description = "SSO服务商标识") String provider,
                                      ServerWebExchange exchange) {
        return doHandleNotify(provider, exchange, exchange.getRequest().getQueryParams().toSingleValueMap());
    }

    @PostMapping(value = "/{provider}/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @Operation(summary = "(POST)登录结果通知并跳转页面")
    public Mono<Void> handlePostNotify(@PathVariable
                                       @Parameter(description = "SSO服务商标识") String provider,
                                       ServerWebExchange exchange) {
        return exchange
            .getFormData()
            .map(MultiValueMap::toSingleValueMap)
            .flatMap(param -> doHandleNotify(provider, exchange, param));
    }


    @GetMapping("/{provider}/notify-result")
    @Operation(summary = "(GET)获取SSO登录结果")
    public Mono<BindResult> handleGetNotifyResult(@PathVariable
                                                  @Parameter(description = "SSO服务商标识") String provider,
                                                  ServerWebExchange exchange) {
        return doHandleNotify(provider, exchange.getRequest().getQueryParams().toSingleValueMap());
    }

    @PostMapping(value = "/{provider}/notify-result", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @Operation(summary = "(POST Form)获取SSO登录结果")
    public Mono<BindResult> handlePostNotifyResult(@PathVariable
                                                   @Parameter(description = "SSO服务商标识") String provider,
                                                   ServerWebExchange exchange) {
        return exchange
            .getFormData()
            .map(MultiValueMap::toSingleValueMap)
            .flatMap(param -> doHandleNotify(provider, param));
    }

    @PostMapping(value = "/{provider}/notify-result", consumes = MediaType.APPLICATION_JSON_VALUE)
    @Operation(summary = "(POST JSON)获取SSO登录结果")
    public Mono<BindResult> handleJsonPostNotifyResult(@PathVariable
                                                       @Parameter(description = "SSO服务商标识") String provider,
                                                       @RequestBody Mono<Map<String, Object>> paramMono) {
        return paramMono
            .flatMap(param -> doHandleNotify(provider, param));
    }
}
